import collections
import json
import logging
import sys
import threading
import time
from random import randrange

import requests

from raft.static.log_entity import LogEntity

logging.basicConfig(format='%(asctime)s - %(levelname)s: %(message)s', datefmt='%H:%M:%S', level=logging.INFO)

from raft.static.cluster import Cluster, ELECTION_TIMEOUT_MAX, HEART_BEAT_INTERVAL, Node

cluster = Cluster()


class RaftNode(threading.Thread):
    def __init__(self, node_id):
        threading.Thread.__init__(self)

        self.cluster = Cluster()
        if node_id + 1 > len(cluster):
            node = Node('S' + str(node_id + 1), f'localhost:800{node_id}')
            cluster.join(node)
            cluster.cluster_size += 1
            self.node = node
        else:
            self.node = cluster[node_id]
        self.id = self.node.id
        self.node_state = "Follower"
        self.current_term = 0
        self.votes = []
        self.vote_for = None
        self.leader_id = None
        self.leader_url = None
        self.leader_state = False
        # index of highest log entry known to be replicated on server
        self.match_index = 0
        self.commit_index = 0
        self.commit_state = []
        self.election_timeout = float(
            randrange(ELECTION_TIMEOUT_MAX * node_id, ELECTION_TIMEOUT_MAX * (node_id + 1)))
        self.election_timer = threading.Timer(self.election_timeout, self.become_candidate)
        self.entity = []
        self.entity_dict = {}
        self.heartbeat_timer = None

    def run(self):
        self.become_follower()

    def become_follower(self):
        timeout = float(
            randrange(ELECTION_TIMEOUT_MAX * (int(self.id[1])), ELECTION_TIMEOUT_MAX * (int(self.id[1]) + 1)))
        if self.node_state != 'Follower':
            logging.info(f'{self.id} become follower ... ')
            self.node_state = 'Follower'
        self.votes = []
        self.leader_state = False
        self.election_timer.cancel()
        self.election_timer = threading.Timer(timeout, self.become_candidate)
        self.election_timer.start()
        logging.info(f'{self.id} reset election timer {timeout} s ... ')

    def vote(self, vote_request):
        logging.info(f'{self.id} got vote request: {vote_request} ')
        vote_result = self.request_vote(vote_request)
        logging.info(f'{self.id} return vote result: {vote_result} ')
        return vote_result

    def become_candidate(self):
        logging.warning(f'heartbeat is timeout: {int(self.election_timeout)} s')
        logging.info(f'{self.id} become candidate and start to request vote ... ')
        # send_state_update(self.node_state, self.election_timeout)
        self.node_state = "Candidate"
        self.leader_id = None
        self.leader_url = None
        self.votes = []
        self.elect()

    def elect(self):
        """ When become to candidate and start to elect:
            1. term + 1
            2. vote itself
            3. send the vote request (VR) to each peer in parallel
            4. return when it is timeout
        """
        self.current_term = self.current_term + 1
        logging.info(f'{self.id} sends vote request to peers')
        # vote itself
        self.votes.append(self.id)
        vote_request = {
            "candidate_id": self.id,
            "term": self.current_term,
            "lastLogIndex": self.match_index,
            "lastLogTerm": self.current_term - 1
        }
        for peer in [peer for peer in self.cluster if peer != self.node]:
            response = requests.post(f'http://{peer.url}/raft/vote', json=json.dumps(vote_request, ensure_ascii=False))
            result = json.loads(response.text)
            logging.info(f'{self.id} got vote result: {response.status_code}: {result}')
            if result['vote_granted']:  # vote_granted
                self.votes.append(result['id'])  # id
        if self.node_state == 'Candidate':
            if len(self.votes) >= int(len(self.cluster) / 2) + 1:
                self.become_leader()
            else:
                self.become_follower()

    def request_vote(self, vote_request):
        """
        input : candidate (id，current_term,lastLogIndex,lastLogTerm)
        output : vote_granted(true/false),term(current_term,for candidate to update itself)
        rule:
            1. if current_term < lastLogTerm  return false
            2. if (vote_for is None or vote_for == candidate.id) and candidate's log is newer than receiver's return true
            3. if true vote_for == candidate and reset election timeout
        :param vote_request:
        :return:vote_result
        """
        vote_result = {
            'vote_granted': False,
            'id': self.id,
        }
        term = vote_request['term']
        candidate_id = vote_request['candidate_id']
        log_index = vote_request['lastLogIndex']
        if term < self.current_term:
            logging.info(f'{self.id} rejects vote request since term: {term} < {self.current_term}')
            vote_result['vote_granted'] = False
            return vote_result
        if term > self.current_term:
            logging.info(f'{self.id} approves vote request since term: {term} < {self.current_term}')
            self.vote_for = candidate_id
            vote_result['vote_granted'] = True
            return vote_result
        # candidate.term == self.current_term
        if self.vote_for is None or self.vote_for == candidate_id:
            if self.match_index < log_index:
                logging.info(f'{self.id} rejects vote request since logIndex: {log_index} < {self.match_index}')
                vote_result['vote_granted'] = False
                return vote_result
            else:
                self.vote_for = candidate_id
                vote_result['vote_granted'] = True
                return vote_result
        logging.info(f'{self.id} rejects vote request since vote_for: {self.vote_for} != {candidate_id}')
        vote_result['vote_granted'] = False
        return vote_result

    def become_leader(self):
        logging.info(f'{self.id} become leader ... ')
        self.node_state = 'leader'
        self.leader_id = self.node[0]
        self.leader_url = self.node[1]
        self.leader_state = True
        log_info = f'term:{self.current_term}，{self.id} become leader ... '
        log = LogEntity(self.match_index + 1, self.current_term, self.leader_id, log_info)
        self.entity.append(log)
        for peer in [peer for peer in self.cluster if peer != self.node]:
            self.entity_dict[peer.id] = self.get_entity_dict(log.get_json())
        self.heartbeat_timer = threading.Timer(0, self.heartbeat)
        self.heartbeat_timer.start()

    def get_entity_dict(self, log=None, prev_log_index=-1):
        if log is None:
            log = []
        if type(log) != list:
            log = [log]
        entity_dict = {
            'term': self.current_term,
            'leader': self.node,
            'prevLogIndex': self.entity[prev_log_index - 1].index if len(self.entity) > 1 else 0,
            'prevLogTerm': self.entity[prev_log_index - 1].term if len(self.entity) > 1 else 0,
            'entities': log,
            'commitIndex': self.commit_index
        }
        return entity_dict

    def heartbeat(self):
        entity_dict_tmp = {}
        while self.leader_state:
            self.match_index = self.entity[-1].get_index() if self.entity else 0
            logging.info(f'{self.id} send heartbeat to followers')
            logging.info('========================================================================')
            for peer in [peer for peer in self.cluster if peer != self.node]:
                entity_dict_tmp = self.entity_dict[peer.id].copy()
                response = requests.post(f'http://{peer.url}/raft/heartbeat', json=self.entity_dict[peer.id])
                result = json.loads(response.text)
                if response is not None:
                    logging.info(f'{self.id} got heartbeat from follower: {result}')
                    if self.current_term < result['term']:
                        self.become_follower()
                        self.leader_state = False
                        continue
                    if result['log_commit_state']:
                        self.commit_state.append(result['id'])
                        self.entity_dict[peer.id] = self.get_entity_dict()
                    else:
                        log = []
                        prev_log_index = self.entity_dict[peer.id]['prevLogIndex'] - 1
                        entities = self.entity[prev_log_index: -1]
                        for entity in entities:
                            log.append(entity.get_json())
                        self.entity_dict[peer.id] = self.get_entity_dict(log, prev_log_index)
                else:
                    logging.info(f'{self.id} got heartbeat from follower: None')
            logging.info('========================================================================')
            if len(self.commit_state) >= int(len(self.cluster) / 2) + 1:
                print(entity_dict_tmp)
                if entity_dict_tmp['entities']:
                    self.commit_index += 1
                self.commit_state = []
                time.sleep(HEART_BEAT_INTERVAL)
            else:
                continue

    def append_entries(self, entity_dict):
        """
        Arguments:
            term: leader's term
            leaderId: so follower can redirect clients
            prevLogIndex: index of log entry immediately preceding new ones
            prevLogTerm: term of prevLogIndex entry
            entries[]: log entries to store (empty for heartbeat)
            commitIndex: last entry known to be committed
        Results:
            term: currentTerm, for leader to update itself
            success: true if follower contained entry matching prevLogIndex and prevLogTerm
        Rule:
            1.	Return if term < currentTerm
            2.	If term > currentTerm, currentTerm ← term
            3.	If candidate or leader, step down
            4.	Reset election timeout
            5.	Return failure if log doesn’t contain an entry at prevLogIndex whose term matches prevLogTerm
            6.	If existing entries conflict with new entries, delete all
            7.	existing entries starting with first conflicting entry
            8.	Append any new entries not already in the log
        :param entity_dict:
        :return:
        """
        self.match_index = self.entity[-1].get_index() if self.entity else 0
        term = entity_dict['term']
        prev_log_index = entity_dict['prevLogIndex']
        entities = entity_dict['entities']
        if term < self.current_term:
            return False, self.current_term
        else:
            self.vote_for = None
            self.current_term = term
            self.leader_id = entity_dict['leader'][0]
            self.leader_url = entity_dict['leader'][1]
            self.become_follower()
            self.commit_index = entity_dict['commitIndex']
            if not entities:
                return True, self.current_term
            if self.match_index == prev_log_index:
                if self.match_index == 0:
                    for item in entities:
                        log = LogEntity(item['index'], item['term'], item['leaderId'], item['log_info'])
                        if log not in self.entity:
                            self.entity.append(log)
                    self.match_index = self.entity[-1].index if self.entity != [] else 0
                    return True, self.current_term

                elif self.entity[self.match_index - 1].term == entity_dict['prevLogTerm']:
                    for item in entities:
                        log = LogEntity(item['index'], item['term'], item['leaderId'], item['log_info'])
                        if log not in self.entity:
                            self.entity.append(log)
                    self.match_index = self.entity[-1].index if self.entity != [] else 0
                    return True, self.current_term
                if self.entity[self.match_index].term < entity_dict['prevLogTerm']:
                    self.entity.pop()
                    self.match_index -= 1
                    return False, self.current_term
            if self.match_index > prev_log_index:
                self.match_index = prev_log_index
                self.entity = self.entity[0:self.match_index]
                return False, self.current_term
            if self.match_index < prev_log_index:
                return False, self.current_term


if __name__ == '__main__':
    timerThread = RaftNode(int(sys.argv[1]))
    timerThread.start()
